-
Notifications
You must be signed in to change notification settings - Fork 187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for descendant selectors. #61
Conversation
} | ||
}); | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we add the example css output of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
Tried to review where I could. I'm afraid I still have a very limited understanding of how all this stuff works, but most stuff looks good to me :D |
|
||
```js | ||
const styles = StyleSheet.create({ | ||
parent: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both in the original spec you described and in this documentation, it wasn't clear to me whether parent
was just a style name or whether parent
carries some special meaning. It seems to be the former, but is worth clarifying and perhaps using a different name. perhaps someContainer
or something would be clearer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
Thanks for giving this so much thought in order to implement missing functionality while not compromising on the philosophy of Aphrodite, and thanks to @kentcdodds for the quick review! I'm probably going to need to take multiple passes at this, but I'll start with the helpfully thorough PR description. Can you start out the PR description with what this is trying to accomplish, not with what makes it difficult? If I understand correctly, this is specifically to address the fact that children of pseudo selectors are not implementable without hacks in Aphrodite, right? Even starting off by saying it fixes #10 would've been helpful (I see that it's indeed present at the bottom of the description).
The |
classNamesForDescendant); | ||
} | ||
|
||
const generateCSSInner = (selector, style, stringHandlers, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is getting pretty complicated, and could also be greatly aided by a JSdoc, IMO.
Agreed! Thoughts from my first pass: Given your approach, and taking into account the helpful notes from @jlfwong and @kentcdodds, the implementation looks good! I'll try to find some time ASAP to both take a deeper dive into the current implementation and also consider the overall design / approach. |
Just want to say this is a really great idea, especially for the Why not just use |
There was discussion on that in the issue. Because |
bcf550f
to
564fa7f
Compare
Okay, I got around to cleaning up this pull request a bit. Not much changed, but I added a bunch more docs so hopefully that should clear up some confusion. @jlfwong, some responses to your comments:
I added a better intro to the description, hopefully that should be more helpful about what's going on. Sorry, I was approaching this with all of the context in #10!
You are correct, the |
Summary: It turns out descendant selectors are kinda hard. In particular, it's difficult to let *descendants have unique classnames* for themselves, at the same time as allowing *merging between styles which contain descendant selectors*. This pull request attempts to do both of these things. The code is a bit messy, so I'll lay out what's going on in this pull request. Please ask questions, if something is confusing! The Aphrodite code is nice because it's all fairly easily understood, and I'm afraid that this adding too much complexity. An example of the syntax (and an example for the explanation in this commit message), consider: ```js const styles = StyleSheet.create({ parent: { '>>child': { color: "red" }, ':hover': { '>>child': { color: "blue" }, '>>otherchild': { color: "white" } }, }, altparent: { ':hover': { '>>child': { color: "green" } } } }); The basic flow of this diff is: 1. In `StyleSheet.create`, we recurse through the passed-in styles, and find each of the descendant selectors (which have keys that look like `>>blah`). In the example above, it would find `parent['>>child']`, `parent[':hover']['>>child']`, `parent[':hover']['>>otherchild']`, and `altparent[':hover']['>>child']`. In each place, we: - generate a class name for that descendant selector. This is based on the class name of the parent class, as well as the key name. For example, if the class name for `styles.parent` was `parent_abcdef`, we might generate the class name `parent_abcdef__child` for `parent['>>child']`. - tag the style by adding a `_names` object, with the class name as a key of the object. For example, `parent['>>child']` would end up looking like `{ color: "red", _names: { parent_abcdef__child: true } }`. - collect a map of each of the keys (without the `>>` bit) to their class names. For example, for `styles.parent`, we would generate a map that looks like `{ child: "parent_abcdef__child", otherchild: "parent_abcdef__otherchild" }`. We merge in the map from key to class name into the generated style, so that the class names can be accessed using a syntax like `styles.parent.child`. 2. When *parent* styles are passed into `css()`, their styles are merged together. If one style overrides another's descendant styling, the `_names` object will be merged together and will contain all of the associated class names. For example, when evaluating `css(styles.parent, styles.altparent)`, we would end up with merged styles looking like: ``` { '>>child': { color: "red", _names: { parent_abcdef__child: true }, }, ':hover': { '>>child': { color: "green", _names: { parent_abcdef__child: true, altparent_123456__child: true, }, }, '>>otherchild': { color: "white", _names: { parent_abcdef__otherchild: true }, } } } ``` We then generate a map from the descendent keys to all of the class names that could be associated with a given key by recursing and looking at each of the `_names` objects. For example, the map would look like: ``` { '>>child': ["parent_abcdef__child", "altparent_123456__child"], '>>otherchild': ["parent_abcdef__otherchild"] } ``` When generating the styles, we look at this map and then generate styles for each of the classnames listed. This is so that these styles will match up with uses of both `css(styles.parent.child)` and `css(styles.altparent.child)`. For example, when generating the `style[':hover']['>>child']` styles, we generate: ``` .parent_abcdef-o_O-altparent_123456:hover .parent_abcdef__child { ... } .parent_abcdef-o_O-altparent_123456:hover .altparent_123456__child { ... } ``` 3. When *descendant* styles are passed into `css()`, like `css(styles.parent.child)`, we simply return the associated class name (in this case, `"parent_abcdef__child"`) in the output. Fixes #10 Test Plan: - `npm run test` - `cd examples && npm run examples`, then visit http://localhost:4114/ and see that the last line starts green and when "Hover over me" is hovered, the other part turns blue. @zgotsch @jlfwong @kentcdodds @montemishkin
564fa7f
to
56de8c4
Compare
Okay, I pushed a change that uses commas to join things instead of duplicating styles, hopefully that's a little better. I also ran a code coverage tool and found a case I hadn't tested. I'm gonna go back and add tests for the other things we aren't covering right now, maybe we could add a test that ensures aphrodite stays at 100% code coverage! |
You may consider adding a Just some thoughts :-) |
} from './util'; | ||
|
||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awww yeaaahhh JSdoc 💖
FYI, this PR can no longer merge as it has conflicts. |
* | ||
* Example: | ||
* ``` | ||
* generateCSSRuleset(".blah", { color: "red" }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to include an example that involves a pseudo selector so show that selector
isn't necessarily just a classname selector
@@ -49,11 +80,23 @@ const css = (...styleDefinitions) => { | |||
return ""; | |||
} | |||
|
|||
const className = validDefinitions.map(s => s._name).join("-o_O-"); | |||
// Filter out "plain class name" arguments, which just want us to add a | |||
// classname to the end result, instead of generating styles. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's probably helpful to say "this is the case, e.g., for descendent selectors where the relevant CSS is generated by the call to css(styles.foo)
, not the call to `css(styles.foo.someChild)"
Okay, after a second pass, I think I have a better understanding of what this is doing, but am still fuzzy on a few details, and am becoming a little uneasy about some behaviour (if I understand correctly how this works) that would be surprising to me. Here are my major reservations about the API design: If you use a child selector without using the associated parent selector, nothing happens.If I'm understanding the code correctly, if you have the following:
And in your component, you have:
Then the div containing
Because of multiple names, there are multiple ways of referencing the same child.Having multiple ways of accomplishing the same thing generally acts as a red flag of API design to me, but perhaps it's not sensibly avoidable in this case...?
Then, if I'm understanding correctly, the following are equivalent from a user perspective and will generate the same CSS (though not HTML?):
and
and
The semantics of nested descendent selectors is confusing to me...or possibly not implemented correctly? The code goes through all the trouble of identifying what the possibly parent selectors might be, but from the looks of it, it wasn't done recursively? If you have this:
Then what happens if you do this?
Overall, the semantics of any examples involving multiple instances of It's also not intuitive to me that Do others find the semantics from a user-standpoint confusing here? I'm particularly interested in @zgotsch thoughts, who may be able to draw from his experience in a real codebase that resulted in #10 being opened in the first place. From a code complexity standpoint, this change seriously impedes my ability to reason intuitively about what the behavior of the code will be in all cases, but I didn't see any unnecessary complexity given this API design, so it may be necessary evil. |
@jlfwong Thank you for the review of this API! I agree, it's kinda confusing. I'm also super sad that this change makes the code more difficult to understand, so I'd love to figure out an API that resulted in more intuitive code. I couldn't come up with a better way to do it myself, but maybe together we can brainstorm something? Gonna respond to your different points:
That is correct. I'm not super worried about this one though, because it's the same as how normal CSS works, no? It does make the "these descendant selectors are special" more apparent though.
Yeah, this is fugly. You are correct, those are all style-equivalent. I guess I chose this because it seems like it would lead to the least amount of things-not-working. If you have some parent selectors and some associated child selectors, then any combination should Just Work. It seemed simpler from an external API perspective, but leads to complex code.
They're kinda confusing to me too. It wouldn't be too much work to make wrt. your example, I have no idea what happens there. Actually maybe that doesn't work any more now that I implemented the comma stuff. Let's go check! I guess overall, I really like the API for simple cases but gets much more confusing with complex cases. Maybe something similar but simplified would be great? I'm not at all attached to this implementation, it just ended up having a really clean external API (as opposed to the |
Why not using the more sass syntax
|
@iamrane The reasons we're not landing this is because the API is too complex/confusing. Once that gets worked out, we could figure out what syntax we would like to use. |
Any thoughts on using just a space as the child selector instead of two angle brackets ' child': {
color: "red"
} |
@aesopwolf I feel white spaces are dangerous due to the low visibility. Try find a bug where you missed a white space or had the wrong number or wrong kind. It's not fun. Same issue with O/0 and other ambiguous characters. |
Okay, I'm going to close this PR since it's stagnated a bunch and I'm not super enthusiastic that this approach is going to work. All discussion should continue in #10. |
The comments for `generateCSS` included descendant selector example from closed PR Khan#61
The comments for `generateCSS` included descendant selector example from closed PR #61
Summary: This adds support for doing descendant style selection; that is,
changing child component styles based on the state/existence of a parent. This
selection in particularly useful when combined with the use of pseudo-classes,
by generating styles that look like
.parent:hover .child
, so the child stylescan depend on the active state of the parent.
Fixes #10
An example of the syntax that is added in this commit (and an example for the
explanation in this commit message):
It turns out that the API for aphrodite doesn't play well with descendent
styles. In particular, it's difficult to let descendants have unique
classnames for themselves, at the same time as allowing merging between
styles which contain descendant selectors.
This pull request attempts to do both of these things. The code is a bit messy,
so I'll lay out what's going on in this pull request. Please ask questions, if
something is confusing! The Aphrodite code is nice because it's all fairly
easily understood, and I'm afraid that this adding too much complexity.
The basic flow of this diff is:
In
StyleSheet.create
, we recurse through the passed-in styles, and findeach of the descendant selectors (which have keys that look like
>>blah
). Inthe example above, it would find
parent['>>child']
,parent[':hover']['>>child']
,parent[':hover']['>>otherchild']
, andaltparent[':hover']['>>child']
. In each place, we:generate a class name for that descendant selector. This is based on the
class name of the parent class, as well as the key name.
For example, if the class name for
styles.parent
wasparent_abcdef
, wemight generate the class name
parent_abcdef__child
forparent['>>child']
.tag the style by adding a
_names
object, with the class name as a key ofthe object.
For example,
parent['>>child']
would end up looking like{ color: "red", _names: { parent_abcdef__child: true } }
.collect a map of each of the keys (without the
>>
bit) to their classnames.
For example, for
styles.parent
, we would generate a map that looks like{ child: "parent_abcdef__child", otherchild: "parent_abcdef__otherchild" }
.We merge in the map from key to class name into the generated style, so that
the class names can be accessed using a syntax like
styles.parent.child
.When parent styles are passed into
css()
, their styles are mergedtogether. If one style overrides another's descendant styling, the
_names
object will be merged together and will contain all of the associated class
names.
For example, when evaluating
css(styles.parent, styles.altparent)
, we wouldend up with merged styles looking like:
We then generate a map from the descendent keys to all of the class names that
could be associated with a given key by recursing and looking at each of the
_names
objects. For example, the map would look like:When generating the styles, we look at this map and then generate styles for
each of the classnames listed. This is so that these styles will match up with
uses of both
css(styles.parent.child)
andcss(styles.altparent.child)
.For example, when generating the
style[':hover']['>>child']
styles, wegenerate:
When descendant styles are passed into
css()
, likecss(styles.parent.child)
, we simply return the associated class name (in thiscase,
"parent_abcdef__child"
) in the output.Test Plan:
npm run test
cd examples && npm run examples
, then visit http://localhost:4114/ and seethat the last line starts green and when "Hover over me" is hovered, the
other part turns blue.
@zgotsch @jlfwong @kentcdodds @montemishkin